package othello.model;

import othello.players.IPlayer;
import othello.players.IPlayerInfo;
import othello.players.learnplayers.AbstractNeuralNetworkPlayer;
import othello.players.learnplayers.AbstractNeuralNetworkPlayer.BenchmarkState;

import java.awt.Color;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Arrays;
import java.util.Observable;
import javax.swing.SwingUtilities;
import othello.util.GameDetails;

public class GameBoard extends Observable implements IBoard {
	private enum BenchmarkPlayer {BechmarkPlayer1, BechmarkPlayer2, None};
    private int _rows, _cols;
    private List<IPlayer> _players;
    private byte[][] _matrix;
    private GameDetails _stats;
    private int _currPlayer;
    private int _startPlayer = 0;
    private int _moves;
    private int _passes;
    private boolean _hypothetical;
    private Point _lastMove;
    private BenchmarkPlayer benchmarkPlayer = BenchmarkPlayer.None;   
    private BenchmarkState _benchmarkState;
    
    
    
    private List<byte[][]> startingPositionsWhite;
    private List<byte[][]> startingPositionsBlack;
    
    private boolean trainingMovesSet = false;
    
    
    private final int _max_games;
    private final int _max_games_learned;
    private final int _max_games_played;
    private int _current_games = 0;
    private int _current_games_learned = 0;
    private int _current_games_played = 0;
    
    private int _real_max_games;

    public GameBoard(int rows, int cols, GameDetails s, List<byte[][]> startPosW,  List<byte[][]> startPosB) {
        _rows = rows;
        _cols = cols;
        _players = new LinkedList<IPlayer>();
        
        addPlayer(s.getWhitePlayer(), Color.WHITE);
        addPlayer(s.getBlackPlayer(), Color.DARK_GRAY);
        addPlayer(s.getBenchmarkPlayer(), Color.WHITE);	 // Color is changed later on
        
        _benchmarkState = s.getBenchmarkState();
        _hypothetical = false;
        _stats = s;
        _max_games = s.getMaxGames();
        _max_games_learned = s.getMaxGamesToLearn();
        _max_games_played = s.getMaxGamesToPlay();    
        
        _real_max_games = s.getRealMaxGames();    
        
        this._players.get(0).setActive(true);
        this._players.get(1).setActive(true);
        this._players.get(2).setActive(false);
        
        startingPositionsWhite = startPosW;
        startingPositionsBlack = startPosB;
        
        setBenchmarkStates();                
    }
    
    private void switchColors() {
		IPlayer white = getPlayer(Color.WHITE);
		IPlayer black = getPlayer(Color.DARK_GRAY);
		black.setColor(Color.WHITE);
		white.setColor(Color.DARK_GRAY);
	}

    private GameBoard(GameBoard copy) {
        _rows = copy._rows;
        _cols = copy._cols;
        _matrix = new byte[copy._rows][copy._cols];
        for (int r = 0; r < _rows; r++) {
            for (int c = 0; c < _cols; c++) {
                _matrix[r][c] = copy._matrix[r][c];
            }
        }
        _players = new ArrayList<IPlayer>(copy._players.size());
        _players.addAll(copy._players);
        _currPlayer = copy._currPlayer;
        _passes = copy._passes;
        _stats = null;	// We don't need this do we?
        _hypothetical = true;
        _moves = copy._moves;
        _max_games = copy._max_games;
        _max_games_learned = copy._max_games_learned;
        _max_games_played = copy._max_games_played;
        _current_games = copy._current_games;
        _current_games_learned = copy._current_games_learned;
        _current_games_played = copy._current_games_played;
        this.addObserver(_players.get(0));
        this.addObserver(_players.get(1));
        this.addObserver(_players.get(2));
    }
    
	public List<Action> getAvailableActions() {
		ArrayList<Action> availableActions = new ArrayList<Action>();
		for (int row = 0; row < 8; row++) {
		    for (int col = 0; col < 8; col++) {
		        if(isValidMove(getCurrentPlayerColor(), row, col)) {
		            availableActions.add(new Action(row, col));
		        }
		    }
		}        
		return availableActions;
	}
    
    public List<IPlayer> getPlayersList() {
    	return this._players;
    }
    

    public int getMoves() {
        return _moves;
    }
    
    public int getGamesLeft() {
    	return this._max_games - this._current_games;
    }

    public byte[][] getState() {
        return _matrix;
    }

    private IPlayer getPlayer(Color c) {
        for (int i = 0; i < _players.size(); i++) {
            IPlayer p = _players.get(i);
            if (p.getColor().equals(c) && p.isActive()) {
                return p;
            }
        }
        return null;
    }
    
    private int getPlayerID(Color c) {
        for (int i = 0; i < _players.size(); i++) {
            IPlayer p = _players.get(i);
            if (p.getColor().equals(c) && p.isActive()) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public void beginGame() {
    	_matrix = new byte[_rows][_cols];
        _passes = 0;
        _lastMove = null;
        initBoard();
        
        // Go to the next player and skip the inactive players (the benchmarkplayer)
        if (!_hypothetical) {
            //~ if (this._players.get((_startPlayer + 1) % _players.size()).isActive()) {
            	//~ _startPlayer = (_startPlayer + 1) % _players.size();
            //~ 
            //~ } else {
            	//~ _startPlayer = (_startPlayer + 2) % _players.size();            
            //~ }
            
            switchColors();
            //~ _currPlayer = _startPlayer;
            _currPlayer = getPlayerID(Color.DARK_GRAY);
            
    	}
        
        setBenchmarkState();
        _current_games++;       			
        
        if (this._benchmarkState == BenchmarkState.Benchmarking) {
			//~ if (_current_games_played % 2 == 0)
				//~ _matrix = copyArray(startingPositionsWhite.get(_current_games_played / 2));
			//~ else
				//~ _matrix = copyArray(startingPositionsBlack.get(_current_games_played / 2));
			
			_matrix = copyArray(startingPositionsBlack.get(
						(_current_games_played / 2) % startingPositionsBlack.size()));
			//~ System.out.println("Board: " + (_current_games_played / 2) % startingPositionsBlack.size() );
			//~ print();			
			_moves = 12;
		}
		
		if (this._benchmarkState == BenchmarkState.Training) {
			//~ if (_current_games_learned % 2 == 0)
				//~ _matrix = copyArray(startingPositionsWhite.get(
					//~ (_current_games_played / 2) % startingPositionsWhite.size()));
			//~ else
				//~ _matrix = copyArray(startingPositionsBlack.get(
					//~ (_current_games_played / 2) % startingPositionsBlack.size()));		
			_matrix = copyArray(startingPositionsBlack.get(
						(_current_games_learned / 2) % startingPositionsBlack.size()));
			//~ System.out.println("Board: " + _current_games_learned / 2);
			//~ print();
			_moves = 12;
		}
		
		//~ System.out.println(_players.get(_currPlayer).getColor() + " starts, player " + _currPlayer);
		
        beginTurn();
    }
    
    private byte[][] copyArray(byte[][] arr) {
		byte[][] return_arr = new byte[_rows][_cols];		
		for(int row = 0; row != _rows; ++row)
			for(int col = 0; col != _cols; ++col)
				return_arr[row][col] = arr[row][col];
		return return_arr;		
	}
	
	@Override
	public int getRealMaxGames() {
		return _real_max_games;
	}	
    
    private void setBenchmarkStates() {
    	for(IPlayer iplayer : this._players) {
            if (iplayer instanceof AbstractNeuralNetworkPlayer) {
    			((AbstractNeuralNetworkPlayer) iplayer).setState(this._benchmarkState);
    		}
    	}
    }

    private void initBoard() {
        int rMid = _rows / 2;
        int cMid = _cols / 2;
        _matrix[rMid][cMid] = -1;
        _matrix[rMid - 1][cMid - 1] = -1;
        _matrix[rMid - 1][cMid] = 1;
        _matrix[rMid][cMid - 1] = 1;
        _moves = 4;
    }

    public void beginTurn() {
        IPlayer player = (_players.get(_currPlayer));
        player.update(this, GameState.BEGINTURN);
    }

    @Override
    public int getColCount() {
        return _cols;
    }

    @Override
    public int getRowCount() {
        return _rows;
    }

    @Override
    public boolean placePiece(IPlayer player, int row, int col) {
        // Make sure the correct player is playing.
        if (isGameOver() || player != _players.get(_currPlayer)) {
            return false;
        }

        // Make sure the coordinates are valid.
        if (row < 0 || row >= _rows || col < 0 || col >= _cols) {
            throw new IllegalArgumentException();
        }

        // See if the spot is free.  If not, fail.
        byte p = _matrix[row][col];
        if (p != 0) {
            //System.out.println("  Denied!  That space is not free!");
            return false;
        }

        // Make the move.
        if(player.getColor() == Color.DARK_GRAY) {
            _matrix[row][col] = -1;
        } else if(player.getColor() == Color.WHITE) {
            _matrix[row][col] = 1;
        }

        // REVIEW: When a player has no valid moves, they need to be able to pass.
        int flips = 0;
        if (walkVector(row, col, -1, 0, player, false) > 0) {
            flips += walkVector(row, col, -1, 0, player, true);
        }
        if (walkVector(row, col, -1, 1, player, false) > 0) {
            flips += walkVector(row, col, -1, 1, player, true);
        }
        if (walkVector(row, col, 0, 1, player, false) > 0) {
            flips += walkVector(row, col, 0, 1, player, true);
        }
        if (walkVector(row, col, 1, 1, player, false) > 0) {
            flips += walkVector(row, col, 1, 1, player, true);
        }
        if (walkVector(row, col, 1, 0, player, false) > 0) {
            flips += walkVector(row, col, 1, 0, player, true);
        }
        if (walkVector(row, col, 1, -1, player, false) > 0) {
            flips += walkVector(row, col, 1, -1, player, true);
        }
        if (walkVector(row, col, 0, -1, player, false) > 0) {
            flips += walkVector(row, col, 0, -1, player, true);
        }
        if (walkVector(row, col, -1, -1, player, false) > 0) {
            flips += walkVector(row, col, -1, -1, player, true);
        }

        if (flips == 0) {
            // The move wasn't really valid, so revert.
            _matrix[row][col] = 0;
            return false;
        }

        // The move was valid.        
        _moves++;
    	_passes = 0;
        _lastMove = new Point(col, row);        

        if (!_hypothetical) {        	
            player.update(this, GameState.ENDTURN);
        }

        nextTurn();
        return true;
    }

    @Override
    public GameBoard hypotheticallyPlacePiece(Color playerColor, int row, int col) {
        GameBoard b = new GameBoard(this);
        IPlayer player = getPlayer(playerColor);
        b.placePiece(player, row, col);
        return b;
    }

    @Override
    public boolean pass(IPlayer player) {
        // Make sure the correct player is playing.
        if (isGameOver() || player != _players.get(_currPlayer)) {
            //			assert (false) : "It is not " + player + "'s turn!!!!!";
            return false;
        }

        if (hasValidMove(player)) {
            // Player is not allowed to pass when they don't have to.
            return false;
        }

        // The pass was accepted.
        // Note that once everyone has passed (consecutively), the game is over.
        _passes++;
        _lastMove = null;

        if (!_hypothetical) {
            player.update(this, GameState.ENDTURN);
        }

        nextTurn();
        return true;
    }

    @Override
    public boolean isValidMove(Color p, int row, int col) {
        if (row < 0 || row >= _rows || col < 0 || col >= _cols || _matrix[row][col] == 1 || _matrix[row][col] == -1) {
            return false;
        }

        // A move is valid if it would flip at least one opposing piece.
        int flips = 0;
        flips += Math.max(0, walkVector(row, col, -1, 0, p));
        flips += Math.max(0, walkVector(row, col, -1, 1, p));
        flips += Math.max(0, walkVector(row, col, 0, 1, p));
        flips += Math.max(0, walkVector(row, col, 1, 1, p));
        flips += Math.max(0, walkVector(row, col, 1, 0, p));
        flips += Math.max(0, walkVector(row, col, 1, -1, p));
        flips += Math.max(0, walkVector(row, col, 0, -1, p));
        flips += Math.max(0, walkVector(row, col, -1, -1, p));
        return flips > 0;
    }

    private int walkVector(int r0, int c0, int dR, int dC, Color player) {
        IPlayer p = getPlayer(player);
        if (p == null) {
            return 0;
        }
        return walkVector(r0, c0, dR, dC, p, false);
    }

    private int walkVector(int r0, int c0, int dR, int dC, IPlayer player, boolean flip) {
        int r = r0 + dR;
        int c = c0 + dC;
        int steps = 0;

        while (r >= 0 && r < _rows && c >= 0 && c < _cols) {
            byte p = _matrix[r][c];

            // Empty squares are treated like the edge of the board.
            if (p == 0) {
                break;
            }
            // Squares already occupied by the player end the walk.
            // Return the number of opposing squares in between.
            if(player.getColor() == Color.DARK_GRAY) {
                if (p == -1) {
                    return steps;
                }
            } else if (player.getColor() == Color.WHITE) {
                if (p == 1) {
                    return steps;
                }
            }
            
            // If we're flipping, flip.
            if (flip) {
                if(player.getColor() == Color.DARK_GRAY) {
                    _matrix[r][c] = -1;
                } else if(player.getColor() == Color.WHITE) {
                    _matrix[r][c] = 1;
                }
            }
            // Walk onward.
            r += dR;
            c += dC;
            steps++;
        }

        // We ran into the edge of the board or an empty square before
        // encountering another square owned by this player.
        return -1;
    }

    private boolean hasValidMove(IPlayer player) {
        // See if the player has any valid moves at all.
        for (int r = 0; r < getRowCount(); r++) {
            for (int c = 0; c < getColCount(); c++) {
                if (isValidMove(player.getColor(), r, c)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private IPlayer findActivePlayer(Color color) {
    	for (IPlayer player : this._players) {
    		if (player.isActive() && player.getColor().equals(color))
    			return player;
    	}
    	return null;
    }

    private void nextTurn() {
        if (isGameOver()) {        	
        	if (_stats != null) {
                //~ int b = playerScore(_stats.getBlackPlayer().getColor());
                //~ int w = playerScore(_stats.getWhitePlayer().getColor());
                int b = playerScore(Color.DARK_GRAY);
                int w = playerScore(Color.WHITE);
                
                if (benchmarkPlayer == BenchmarkPlayer.BechmarkPlayer2) {
                	// Swap the values, because the first player
                	// is now the benchmark player
                	int temp = b;
                	b = w;
                	w = temp;
                }
                
                //~ System.out.println("Black: " + b + ", white: " + w);
                //~ System.out.println(getGameScore());
                
                if(b > w) {
                	findActivePlayer(Color.WHITE).lost(this._benchmarkState);
                	findActivePlayer(Color.DARK_GRAY).won(this._benchmarkState);
                } else if (b == w) {
                	findActivePlayer(Color.WHITE).tied(this._benchmarkState);
                	findActivePlayer(Color.DARK_GRAY).tied(this._benchmarkState);
                } else {
                	findActivePlayer(Color.WHITE).won(this._benchmarkState);
                	findActivePlayer(Color.DARK_GRAY).lost(this._benchmarkState);
                }
                _stats.nextGame();
                
            }
        	
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    // Notify the next player.
                    onGameOver();
                }
            });
            return;
        }
        
        // Go to the next player and skip the inactive players
        if (!_hypothetical) {
	        if (this._players.get((_currPlayer + 1) % _players.size()).isActive()) {
	            _currPlayer = (_currPlayer + 1) % _players.size();
	        } else {
	            _currPlayer = (_currPlayer + 2) % _players.size();
	        }
        }
        
        if (!_hypothetical) {
            // Notify the next player.
            beginTurn();
        }
    }
    
    public BenchmarkState getBenchmarkstate(){
    	return _benchmarkState;
    }
    
    static int saves = 0;
    
    private void setBenchmarkState() {
    	// If playing, don't train, learn, benchmark etc, just play :)
        if (this._benchmarkState == BenchmarkState.Playing)
        	return;
        
        // Check if in training mode
        if (this._benchmarkState == BenchmarkState.Training) {
            // Check if max games to learn reached
        	if (this._current_games_learned == this._max_games_learned - 1 ||
    			this._max_games_learned == 0 || this._current_games == 0) {
        		// Check if first player is active
        		if (_players.get(0).isActive()) {
        			// If true, then disable the second player
        			// and benchmark with the first
                	_players.get(0).setActive(true);
                	_players.get(0).setColor(Color.WHITE);
                    _players.get(1).setActive(false);
                    _players.get(2).setActive(true);
                    _players.get(2).setColor(Color.DARK_GRAY);
                    //~ _stats.getBenchmarkPlayer().setColor(Color.DARK_GRAY);
                    
                    benchmarkPlayer = BenchmarkPlayer.BechmarkPlayer1;
        		}
        		
        		// Reset the play results
                for (IPlayer player : _players)
                	player.resetPlayResults();
                
        		// Go to benchmark mode
        		this._benchmarkState = BenchmarkState.Benchmarking;        		
        		setBenchmarkStates();
        		this._current_games_played = 0;
        	} else {
        		// Continue with training mode
        		this._current_games_learned++;
        	}
        } else if (this._benchmarkState == BenchmarkState.Benchmarking) {
        	// Check if max games to play reached        	
        	if (this._current_games_played == this._max_games_played - 1 ||
    			this._max_games_played == 0) {
                // Check if second player is active
                //~ if (_players.get(0).isActive()) {
                	//~ // Store the results and reset the play results
	                //~ for (IPlayer player : _players) {
	                	//~ if (player.isActive()) {
		                	//~ player.storeRatio();
		                	//~ player.resetPlayResults();
	                	//~ }
	                //~ }
	                //~ 
				    //~ // If not, then disable the first player
				    //~ // and benchmark with the second
	                //~ _stats.getBenchmarkPlayer().setColor(Color.WHITE);
	                //~ benchmarkPlayer = BenchmarkPlayer.BechmarkPlayer2;
	                //~ _players.get(0).setActive(false);
                    //~ _players.get(1).setActive(true);
                    //~ _players.get(2).setActive(true);
	                //~ 
                    //~ // Set back games played because we test the other player
                    //~ _current_games_played = 0;
                    //~ _current_games -= this._max_games_played;
                //~ } else {
                	// Store the results and reset the play results
				for (IPlayer player : _players) {
					if (player.isActive()) {
						player.storeRatio();
						player.resetPlayResults();		                	
					}
				}
				
				// Disable benchmark
				_players.get(0).setActive(true);
				_players.get(0).setColor(Color.WHITE);
				_players.get(1).setActive(true);
				_players.get(1).setColor(Color.DARK_GRAY);
				_players.get(2).setActive(false);
				
				// Go to learn mode
				benchmarkPlayer = BenchmarkPlayer.None;
				_benchmarkState = BenchmarkState.Training;
				setBenchmarkStates();
				_current_games_learned = 0;                
        	} else {
        		// Continue with playing mode
        		this._current_games_played++;
        	}                
        }
    }

    @Override
    public boolean isGameOver() {
        return _moves >= _rows * _cols || _passes >= (_players.size() - 1);
    }

    @Override
    public Color playerAt(int row, int col) {
        if (_matrix == null) {
            return null;
        }

        if (row < 0 || row >= _rows || col < 0 || col >= _cols) {
            throw new IllegalArgumentException();
        }

        byte p = _matrix[row][col];
        if (p == 0) {
            return null;
        } else if(p == -1) {
            return Color.DARK_GRAY;
        } else if(p == 1) {
            return Color.WHITE;
        }
        return null;
    }
    
    public void print() {
		System.out.print(' ');
		for (int r = 0; r < _rows; r++)
			System.out.print(r);
		System.out.print('\n');
		for (int r = 0; r < _rows; r++) {			
			System.out.print(r);
            for (int c = 0; c < _cols; c++) {
				if (_matrix[r][c] == -1)
					System.out.print('x');
				else if (_matrix[r][c] == 1)
					System.out.print('o');
                else
					System.out.print(' ');                
            }
            System.out.println();
        }
    }	
		

    @Override
    public int playerScore(Color p) {
        if (_matrix == null) {
            return 0;
        }
        int score = 0;

        byte pl = 0;
        if(p == Color.DARK_GRAY) {
            pl = -1;
        } else if(p == Color.WHITE) {
            pl = 1;
        }

        for (int r = 0; r < _rows; r++) {
            for (int c = 0; c < _cols; c++) {
                if (_matrix[r][c] != 0 && _matrix[r][c] == pl) {
                    score++;
                }
            }
        }
        return score;
    }

    @Override
    public void addPlayer(IPlayer p, Color color) {
        this.addObserver(p);
        _players.add(p);
        p.joinBoard(this, color);
    }

    @Override
    public Iterator<IPlayerInfo> getPlayers() {
        return new PlayerIterator();
    }

    private class PlayerIterator implements Iterator<IPlayerInfo> {
        /**
         * Counts from the index of the current player up to current + num-players.
         * That is, if it is currently player 2's turn out of 4 players, it
         * will count from 2 until 5.  Mod then makes the indeces valid.
         */
        private int _index;

        public PlayerIterator() {
            _index = _currPlayer;
        }

        @Override
        public boolean hasNext() {
            return _index < _currPlayer + (_players.size() - 1);
        }

        @Override
        public IPlayerInfo next() {
            final IPlayer player = _players.get(_index++ % (_players.size() - 1));

            // Return a copy of the player info, so caller can neither change
            // it, nor cast it to an IPlayer, nor use it to call placePiece().
            return new IPlayerInfo() {

                @Override
                public String getName() {
                    return player.getName();
                }

                @Override
                public Color getColor() {
                    return player.getColor();
                }
            };
        }

        @Override
        public void remove() {
        }
    }

    @Override
    public void beginTournament() {
        // Kick off the first game.        
        onGameOver();
    }

    private void onGameOver() {
        this.setChanged();
        // Notify players of endgame
        this.notifyObservers(GameState.ENDGAME);
    	
        // Start another game if appropriate
        if (this._current_games <= this._max_games) {
            // Each game, a different player will start.
            // The board layout will remain the same though.
        	beginGame(); 
        } else {
        	if (!_hypothetical) {
	        	// Notify players for end of training to save ANN
	        	this.setChanged();
	        	this.notifyObservers(GameState.SAVETRAINING);
	        	
	        	// Stores the ratio's of the two players (not of the benchmark player)
	        	this._players.get(0).saveRatios();
	        	//~ this._players.get(1).saveRatios();
        	}
        }
    }

    @Override
    public String getCurrentPlayerScore() {
        if (isGameOver()) {
            return null;
        }
        IPlayer plyr = _players.get(_currPlayer);
        return plyr.getName() + "'s turn (" + playerScore(plyr.getColor()) + ").";
    }

    @Override
    public Color getCurrentPlayerColor() {
        return (_players.get(_currPlayer)).getColor();
    }
    
    public int getCurrentPlayer() {
		return _currPlayer;
	}
		

    @Override
    public String getGameScore() {
        if (!isGameOver()) {
            return null;
        }

        String score = "";
        for (int i = 0; i < _players.size(); i++) {
            IPlayer plyr = _players.get(i);
            if (i >= 0) {
                score += "   ";
            }
            score += plyr.getName() + ": " + playerScore(plyr.getColor()) + ".";
        }
        return score;
    }

    @Override
    public String getTournamentScore() {
        if (_stats == null) {
            return null;
        }

        return _stats.toString();
    }
    
    public int getTurns() {
        return _moves + _passes - 4;
    }

    @Override
    public Point lastMove() {
        return _lastMove;
    }
    
    public int getRun(){
		return _stats.getRun();
	}
	
	public boolean getRandFirstFourMoves(){
		return _stats.getRandFirstFourMoves();
	}
		
    
    @Override
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if((obj == null) || (obj.getClass() != this.getClass())) {
            return false;
        }
        GameBoard gameBoard = (GameBoard) obj;
    	boolean equal = true;
    	
        for(int i=0;i < gameBoard._matrix.length;i++) {
                for(int k=i;k < gameBoard._matrix[i].length;k++) {
                        if (gameBoard._matrix[i][k] != this._matrix[i][k]) {
                                equal = false;
                        }
                }
        }
    	
    	return equal;
    }
}
